57  绘制条形图

本章定位(复习与补充训练层):本章对应正课第20章《常用图形绘制》(章节 20,“分类比较用柱状图/条形图”的选型与绘制),并与第49章《数据框可视化》(章节 50)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。

57.1 引言分类数据的可视化

条形图(Bar Plot)是展示分类数据最直观的工具,金融分析中用于不同类别间的量值比较、Top N 排名展示、同一指标不同时期的对比以及构成分析。

57.2 本章学习目标

通过本章复习,你将能够:

  1. 区分 plt.bar(x, height)plt.barh(y, width) 的参数含义与适用场景,并根据类别名称的长短与数量选择纵向或横向
  2. 复述平台任务的数据流:用 np.array 按行组织各期收盘价、pd.DataFrame(data=..., index=..., columns=...) 生成数据框、index_data/index_data.shift(1)-1 计算月度涨跌幅、dropna 后用 iloc 取某一行画一组条形
  3. 说明分组条形图的位置偏移(x±width)、堆叠条形图的 bottom 累加逻辑,以及百分比堆叠图的口径转换
  4. 用条形图展示杜邦分析三因子,并解释 \(\text{ROE}\) 的三因素分解

先修:章节 20章节 10(第20章的柱状图/条形图绘制与第10章的DataFrame构造,本章平台任务的数据组织依赖二者)。

57.3 条形图 vs 直方图

特性 条形图 直方图
数据类型 分类变量 数值变量
X轴 离散类别 连续区间
条形宽度 可调整,有间隔 固定,无间隔
排列顺序 任意(通常按值排序) 固定(按区间边界)
应用场景 比较、排名 分布形态

57.4 基础条形图

任务要求(归纳自块内任务注释与代码):用列表给出 5 个月末时点与 4 个指数名称,用 np.array 按行组织各月末收盘价,构造行=月份、列=指数的 DataFrame;用 index_data/index_data.shift(1)-1 计算月度涨跌幅,dropna 去掉首月后用 print 输出。请将代码原样输入教学平台(注释除外),判定以平台为准。

平台任务1(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 57.1: 平台原始代码 (1)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库

date=["2019年4月","2019年5月","2019年6月","2019年7月","2019年8月"]#创建一个日期的列表

name=['标普500指数','富时100指数','恒生指数','上证综指']#创建一个指数名称的列表

# 创建NumPy数组closeprice
closeprice=np.array([[2945.83,7418.22,29699.11,3078.34],[2752.06,7161.71,26901.09,

2898.70],[2941.76,7425.63,28542.62,2978.88],[2980.38,7586.78,27777.75,2932.51],  # 续行:closeprice=np.array(的数据项

[2926.46,7207.18,25724.73,2886.24]]) #创建收盘价的数据框

index_data = pd.DataFrame(data=closeprice,index=date,columns=name) #生成月末收盘价的数据框

index_return = index_data/index_data.shift(1)-1  #生成月度涨跌幅的数据框

index_return = index_return.dropna() #删除缺失值

print(index_return)  # 输出收益率数据

预期输出(本机实际运行结果,数据内联于代码中;以平台运行结果为准):

          标普500指数   富时100指数      恒生指数      上证综指
2019年5月 -0.065778 -0.034578 -0.094212 -0.058356
2019年6月  0.068930  0.036852  0.061021  0.027661
2019年7月  0.013128  0.021702 -0.026797 -0.015566
2019年8月 -0.018092 -0.050034 -0.073909 -0.015778

注意:代码中的日期列表虽含 2019 年 4 月,但 4 月为基准期,shift(1) 后首行全 NaN 被 dropna 删除,输出只剩 5~8 月四行。

任务要求(归纳自块内任务注释与代码):沿用任务一的月度涨跌幅数据,在 2×2 子图中分别绘制 2019 年 5~8 月各月四个指数的涨跌幅柱状图(宽度 0.5,依次黄、青、蓝、蓝着色),子图间共用 X/Y 轴,设置轴标签、图例与网格,保存为 1.png。请将代码原样输入教学平台(注释除外),判定以平台为准。

平台任务2(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 57.2: 平台原始代码 (2)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
from pylab import mpl  # 导入mpl模块
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
plt.rcParams['font.sans-serif']=['SimHei']  # 用黑体显示中文
plt.rcParams['axes.unicode_minus']=False   # 正常显示负号

date=["2019年4月","2019年5月","2019年6月","2019年7月","2019年8月"]#创建一个日期的列表
name=['标普500指数','富时100指数','恒生指数','上证综指']#创建一个指数名称的列表
# 创建NumPy数组closeprice
closeprice=np.array([[2945.83,7418.22,29699.11,3078.34],[2752.06,7161.71,26901.09,
2898.70],[2941.76,7425.63,28542.62,2978.88],[2980.38,7586.78,27777.75,2932.51],  # 续行:closeprice=np.array(的数据项
[2926.46,7207.18,25724.73,2886.24]]) #创建收盘价的数据框
index_data = pd.DataFrame(data=closeprice,index=date,columns=name) #生成月末收盘价的数据框
index_return = index_data/index_data.shift(1)-1  #生成月度涨跌幅的数据框
index_return = index_return.dropna() #删除缺失值
plt.figure(figsize=(11,10))  # 创建图形画布
plt.subplot(2,2,1) #第1行、第1列子图
# 绘制柱状图
plt.bar(x=index_return.columns,height=index_return.iloc[0],width=0.5,label=u"2019年5月涨跌幅",facecolor="y")
plt.xticks (fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"涨跌幅",fontsize=13,rotation=90)  # 设置Y轴标签
plt.legend(loc=9,fontsize=13) #图列放置在中上位置
plt.grid(True)  # 显示网格线
plt.subplot(2,2,2,sharex=plt.subplot(2,2,1),sharey=plt.subplot(2,2,1)) #与第一个子图个的x轴和Y轴相同
# 绘制柱状图
plt.bar(x=index_return.columns,height=index_return.iloc[1],width=0.5,label=u"2019年6月涨跌幅",facecolor="c")
plt.xticks (fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.legend(loc=8,fontsize=13) #图列放置在中下位置
plt.ylabel(u"涨跌幅",fontsize=13,rotation=90)  # 设置Y轴标签
plt.grid(True)  # 显示网格线
plt.subplot(2,2,3,sharex=plt.subplot(2,2,1),sharey=plt.subplot(2,2,1))  # 选择子图位置
# 绘制柱状图
plt.bar(x=index_return.columns,height=index_return.iloc[2],width=0.5,label=u"2019年7月涨跌幅",facecolor="b")
plt.xticks (fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.legend(loc=9,fontsize=13)  # 添加图例
plt.ylabel(u"涨跌幅",fontsize=13,rotation=90)  # 设置Y轴标签
plt.grid(True)  # 显示网格线
plt.subplot(2,2,4,sharex=plt.subplot(2,2,1),sharey=plt.subplot(2,2,1))  # 选择子图位置
# 绘制柱状图
plt.bar(x=index_return.columns,height=index_return.iloc[3],width=0.5,label=u"2019年8月涨跌幅",facecolor="b")
plt.xticks (fontsize=13)  # 设置X轴刻度标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.legend(loc=9,fontsize=13)  # 添加图例
plt.ylabel(u"涨跌幅",fontsize=13,rotation=90)  # 设置Y轴标签
plt.grid(True)  # 显示网格线
plt.savefig("1.png")  # 保存图形至文件

预期输出(本机实际运行结果,数据内联于代码中;以平台运行结果为准):本块无控制台输出,生成文件 1.png——2×2 子图各画四根柱子(四个指数),5 月子图四根柱子全为负值(黄),6 月全为正值(青),7 月正负混合、8 月全为负值(蓝);各子图共用 X/Y 轴刻度,便于逐月横向对比。

任务要求(归纳自块内任务注释与代码):用 plt.barh 以横向条形图对比 2019 年 6 月与 7 月两个月各指数的涨跌幅(Y 轴为指数名称,两组条形各高 0.5),设置 X 轴标签”涨跌幅”、标题与图例(loc=0 自动选位),显示图形并保存为 2.png。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 57.3: 平台原始代码 (3)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
from pylab import mpl  # 导入mpl模块
 
plt.rcParams['font.sans-serif']=['SimHei']   # 用黑体显示中文
plt.rcParams['axes.unicode_minus']=False     # 正常显示负号

date=["2019年4月","2019年5月","2019年6月","2019年7月","2019年8月"]#创建一个日期的列表
name=['标普500指数','富时100指数','恒生指数','上证综指']#创建一个指数名称的列表
# 创建NumPy数组closeprice
closeprice=np.array([[2945.83,7418.22,29699.11,3078.34],[2752.06,7161.71,26901.09,
2898.70],[2941.76,7425.63,28542.62,2978.88],[2980.38,7586.78,27777.75,2932.51],  # 续行:closeprice=np.array(的数据项
[2926.46,7207.18,25724.73,2886.24]]) #创建收盘价的数据框
index_data = pd.DataFrame(data=closeprice,index=date,columns=name) #生成月末收盘价的数据框
index_return = index_data/index_data.shift(1)-1  #生成月度涨跌幅的数据框
index_return = index_return.dropna() #删除缺失值
plt.figure (figsize=(8,5))  # 创建图形画布
plt.barh(y=index_return.columns,width=index_return.iloc[1],height=0.5,label=u'2019年6月涨跌幅')  # 绘制柱状图
plt.barh(y=index_return.columns,width=index_return.iloc[2],height=0.5,label=u'2019年7月涨跌幅')  # 绘制柱状图
plt.xticks(fontsize=13)  # 设置X轴刻度标签
plt.xlabel(u"涨跌幅",fontsize=13)  # 设置X轴标签
plt.yticks (fontsize=13)  # 设置Y轴刻度标签
plt.title(u"比较4个股指在2019年6月和7月的月度涨跌幅",fontsize=13)  # 设置图表标题
plt.legend (loc=0,fontsize=13)  # 添加图例
plt.grid(True)  # 显示网格线
plt.show()  # 显示图形
plt.savefig("2.png")  # 保存图形至文件

预期输出(本机实际运行结果,数据内联于代码中;以平台运行结果为准):本块无控制台输出,生成文件 2.png——四个指数各两根左右方向的条形,同月的两组条形以图例区分:6 月四根全部向右,最长的是标普500(+0.069);7 月恒生(-0.027)与上证(-0.016)两根向左、标普与富时两根向右,即恒生与上证由涨转跌;先画的 6 月组条形会被后画的 7 月组部分遮挡,这是同一起点上重复绘制横向条形的固有读图注意点。

关键参数解析:

  1. color:条形颜色,使用专业配色
  2. alpha:透明度,0.8使图表不显沉重
  3. edgecolor:条形边框,白色增强分隔感
  4. 数值标签:在每个条形顶部标注具体值

57.5 水平条形图

列表 57.4
# =============================================================================
# 题目:水平条形图——股票收益率排名
# =============================================================================
# 本任务演示如何绘制水平条形图,特别适合类别名称较长的情况
# 应用场景:股票排名、基金业绩对比

# ==================== 创建股票数据 ====================
stocks = ['贵州茅台', '五粮液', '招商银行', '中国平安', '平安银行',
          '工商银行', '建设银行', '农业银行', '中国银行', '交通银行']
returns = [0.05, 0.03, 0.02, 0.015, 0.018,
           0.012, 0.011, 0.01, 0.009, 0.008]

df_stocks = pd.DataFrame({
    '股票': stocks,
    '收益率': returns
}).sort_values('收益率')  # 按收益率升序排序(从低到高)

# ==================== 绘制水平条形图 ====================
plt.figure(figsize=(10, 8))

# plt.barh()绘制水平条形图(bar horizontal)
# df_stocks['股票']:Y轴数据(类别)
# df_stocks['收益率']:X轴数据(条形长度)
plt.barh(df_stocks['股票'], df_stocks['收益率'],
         # 使用列表推导式根据收益率值选择颜色
         # 收益率 > 3%:红色(突出显示高收益)
         # 收益率 ≤ 3%:青色(普通收益)
         color=['#E3120B' if x > 0.03 else '#008080' for x in df_stocks['收益率']],
         alpha=0.8)

# ==================== 添加数值标签 ====================
# df_stocks.iterrows():遍历DataFrame的每一行
# i:索引(行的位置)
# idx:行索引(原始索引)
# row:行的数据(Series对象)
for i, (idx, row) in enumerate(df_stocks.iterrows()):
    # plt.text()在图上添加文字
    # row['收益率'] + 0.002:X坐标(条形右侧0.002个单位)
    # i:Y坐标(第i个条形的Y位置)
    # f'{row["收益率"]:.1%}':文字内容(百分比格式,保留1位小数)
    # va='center':垂直对齐方式为居中
    plt.text(row['收益率'] + 0.002, i, f'{row["收益率"]:.1%}',
             va='center', fontsize=10)

# ==================== 设置图表装饰 ====================
plt.title('股票收益率排名', fontsize=16, fontweight='bold')
plt.xlabel('收益率', fontsize=12)
plt.ylabel('股票', fontsize=12)
# axis='x':只显示X轴网格线(垂直网格线)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()

# ==================== 输出统计信息 ====================
print('收益率统计:')
print(f'最高: {df_stocks["收益率"].max():.2%}')
print(f'最低: {df_stocks["收益率"].min():.2%}')
print(f'平均: {df_stocks["收益率"].mean():.2%}')

水平 vs 垂直条形图:

场景 推荐类型 理由
类别名称较长 水平 避免标签重叠
类别数量多(>10) 水平 更易阅读
时间序列 垂直 符合时间从左到右的习惯
数值比较 垂直 长度差异更直观

颜色编码技巧:

  • 用不同颜色突出重要类别(如收益率>3%用红色)
  • 保持配色简洁,避免使用过多颜色
  • 参考《经济学人》等专业图表的配色

57.6 分组条形图

列表 57.5
# =============================================================================
# 题目:分组条形图——多指标对比
# =============================================================================
# 本任务演示如何绘制分组条形图,在同一类别中对比多个指标
# 应用场景:年度财务指标对比、多维度绩效评估

# ==================== 创建多年度数据 ====================
years = ['2021', '2022', '2023']
revenue = [100, 120, 135]      # 营业收入(亿元)
profit = [15, 18, 22]         # 净利润(亿元)
margin = [15, 15, 16.3]       # 净利率(%)

# ==================== 准备绘图数据 ====================
# np.arange(len(years))生成X轴位置
# [0, 1, 2]:三个年份的位置
x = np.arange(len(years))

# width:每个条形的宽度
# 0.25是一个经验值,太细不好看,太粗易重叠
width = 0.25

# ==================== 创建图形和轴对象 ====================
fig, ax = plt.subplots(figsize=(10, 6))

# ==================== 绘制分组条形 ====================
# 第一组:营业收入
# x - width:向左偏移一个宽度(第一组位置)
# revenue:条形高度
# width:条形宽度
# label='营业收入':图例标签
# color='#2E86AB':深蓝色
bars1 = ax.bar(x - width, revenue, width, label='营业收入',
               color='#2E86AB', alpha=0.8)

# 第二组:净利润
# x:中心位置(不偏移)
bars2 = ax.bar(x, profit, width, label='净利润',
               color='#E3120B', alpha=0.8)

# 第三组:净利率
# x + width:向右偏移一个宽度(第三组位置)
bars3 = ax.bar(x + width, margin, width, label='净利率(%)',
               color='#008080', alpha=0.8)

# ==================== 添加数值标签 ====================
# 循环遍历三个条形组
for bars in [bars1, bars2, bars3]:
    # 遍历组内的每个条形
    for bar in bars:
        # bar.get_height():获取条形的高度
        height = bar.get_height()

        # bar.get_x() + bar.get_width()/2.:计算条形中心X坐标
        # height:Y坐标(条形顶部)
        # f'{height:.1f}':文字内容(保留1位小数)
        ax.text(bar.get_x() + bar.get_width()/2., height,
                f'{height:.1f}', ha='center', va='bottom', fontsize=9)

# ==================== 设置图表装饰 ====================
ax.set_xlabel('年份', fontsize=12)
ax.set_ylabel('金额(亿元)', fontsize=12)
ax.set_title('财务指标年度对比', fontsize=16, fontweight='bold')

# ax.set_xticks()设置X轴刻度位置
ax.set_xticks(x)

# ax.set_xticklabels()设置X轴刻度标签
ax.set_xticklabels(years)
ax.legend(fontsize=11)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()

# ==================== 计算增长率 ====================
# 2021-2023营业收入增长率
revenue_growth = (revenue[2] - revenue[0]) / revenue[0] * 100
# 2021-2023净利润增长率
profit_growth = (profit[2] - profit[0]) / profit[0] * 100
print(f'2021-2023营业收入增长率: {revenue_growth:.1f}%')
print(f'2021-2023净利润增长率: {profit_growth:.1f}%')

分组条形图的设计要点:

  1. 条形宽度:通常0.2-0.3,太细不好看,太粗易重叠
  2. 间距:组间间距≥组内间距
  3. 颜色:同一组用同一色系,不同组用对比色
  4. 图例:清晰标注每个系列

数学原理: 设 \(n\) 个年份, \(m\) 个指标,第 \(i\) 年第 \(j\) 个指标的条形位置:

\[ x_{ij} = i + (j - \frac{m+1}{2}) \times w \] 其中 \(w\) 是条形宽度。

57.7 堆叠条形图

列表 57.6
# =============================================================================
# 题目:堆叠条形图——收入构成分析
# =============================================================================
# 本任务演示如何绘制堆叠条形图,展示整体及各部分的构成
# 应用场景:收入构成、成本结构、投资组合配置

# ==================== 创建收入构成数据 ====================
years = ['2021', '2022', '2023']
primary_business = [80, 90, 100]      # 主营业务收入(亿元)
other_business = [15, 20, 25]         # 其他业务收入(亿元)
investment_income = [5, 10, 10]       # 投资收益(亿元)

# ==================== 创建图形和轴对象 ====================
fig, ax = plt.subplots(figsize=(10, 6))

# ==================== 绘制堆叠条形图 ====================
# 第一层:主营业务收入(底层)
ax.bar(years, primary_business, label='主营业务收入',
       color='#2E86AB', alpha=0.8)

# 第二层:其他业务收入(堆叠在第一层之上)
# bottom=primary_business:起始高度为第一层的顶部
ax.bar(years, other_business, bottom=primary_business,
       label='其他业务收入', color='#008080', alpha=0.8)

# 第三层:投资收益(堆叠在第二层之上)
# bottom=[p+o for p, o in zip(primary_business, other_business)]:
#   起始高度为前两层之和(使用列表推导式计算)
ax.bar(years, investment_income,
       bottom=[p+o for p, o in zip(primary_business, other_business)],
       label='投资收益', color='#F0A700', alpha=0.8)

# ==================== 添加总数值标签 ====================
for i, year in enumerate(years):
    # total:该年份的总收入(三层之和)
    total = primary_business[i] + other_business[i] + investment_income[i]

    # ax.text()在图上添加文字
    # i:X坐标(第i个条形)
    # total + 5:Y坐标(条形顶部上方5个单位)
    # f'{total}':文字内容(总收入值)
    # ha='center':水平居中
    # va='bottom':垂直底部对齐
    # fontsize=11, fontweight='bold':字号11,加粗
    ax.text(i, total + 5, f'{total}', ha='center', va='bottom',
            fontsize=11, fontweight='bold')

# ==================== 设置图表装饰 ====================
ax.set_xlabel('年份', fontsize=12)
ax.set_ylabel('收入(亿元)', fontsize=12)
ax.set_title('收入构成变化', fontsize=16, fontweight='bold')
# loc='upper left':图例位置在左上角
ax.legend(fontsize=11, loc='upper left')
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()

# ==================== 计算占比变化 ====================
print('各业务占比变化:')
for i, year in enumerate(years):
    total = primary_business[i] + other_business[i] + investment_income[i]
    # 计算各业务占比
    primary_pct = primary_business[i] / total * 100
    other_pct = other_business[i] / total * 100
    invest_pct = investment_income[i] / total * 100
    print(f'{year}: 主营{primary_pct:.1f}%, 其他{other_pct:.1f}%, 投资{invest_pct:.1f}%')

堆叠条形图的应用场景:

  1. 构成分析:展示整体及各部分
  2. 变化追踪:观察构成的时间变化
  3. 对比分析:不同类别的构成差异

注意事项:

  • 不适合比较各部分的大小(基线不统一)
  • 适合展示各部分占总体的比例
  • 考虑用百分比堆叠图使比例更清晰

57.8 百分比堆叠条形图

列表 57.7
# =============================================================================
# 题目:百分比堆叠条形图——投资组合配置
# =============================================================================
# 本任务演示如何绘制百分比堆叠条形图,展示各部分的相对比例
# 应用场景:资产配置变化、市场份额分析

# ==================== 创建资产配置数据 ====================
years = ['2020', '2021', '2022', '2023', '2024']
stocks = [60, 65, 55, 50, 45]      # 股票配置(万元)
bonds = [30, 25, 30, 35, 40]       # 债券配置(万元)
cash = [10, 10, 15, 15, 15]        # 现金配置(万元)

# ==================== 转换为百分比 ====================
# np.array()将列表转换为NumPy数组(便于向量运算)
total = np.array(stocks) + np.array(bonds) + np.array(cash)

# 计算百分比
stocks_pct = np.array(stocks) / total * 100
bonds_pct = np.array(bonds) / total * 100
cash_pct = np.array(cash) / total * 100

# ==================== 创建图形和轴对象 ====================
fig, ax = plt.subplots(figsize=(10, 6))

# ==================== 绘制百分比堆叠条形图 ====================
# 第一层:股票(底层)
ax.bar(years, stocks_pct, label='股票',
       color='#E3120B', alpha=0.8)

# 第二层:债券(堆叠在股票之上)
# bottom=stocks_pct:起始高度为股票百分比
ax.bar(years, bonds_pct, bottom=stocks_pct,
       label='债券', color='#008080', alpha=0.8)

# 第三层:现金(堆叠在债券之上)
# bottom=stocks_pct + bonds_pct:起始高度为股票+债券百分比
ax.bar(years, cash_pct, bottom=stocks_pct + bonds_pct,
       label='现金', color='#F0A700', alpha=0.8)

# ==================== 添加百分比标签 ====================
for i, year in enumerate(years):
    # 股票标签(在第一层中间)
    ax.text(i, stocks_pct[i]/2, f'{stocks_pct[i]:.0f}%',
            ha='center', va='center', color='white', fontweight='bold')

    # 债券标签(在第二层中间)
    ax.text(i, stocks_pct[i] + bonds_pct[i]/2, f'{bonds_pct[i]:.0f}%',
            ha='center', va='center', color='white', fontweight='bold')

    # 现金标签(在第三层中间)
    ax.text(i, stocks_pct[i] + bonds_pct[i] + cash_pct[i]/2, f'{cash_pct[i]:.0f}%',
            ha='center', va='center', color='white', fontweight='bold')

# ==================== 设置图表装饰 ====================
ax.set_xlabel('年份', fontsize=12)
ax.set_ylabel('配置比例(%)', fontsize=12)
ax.set_title('投资组合配置变化', fontsize=16, fontweight='bold')
# ax.set_ylim(0, 100):设置Y轴范围为0-100%
ax.set_ylim(0, 100)
# loc='upper right':图例位置在右上角
# bbox_to_anchor=(1.15, 1):将图例放置在图表外侧(右侧)
ax.legend(fontsize=11, loc='upper right', bbox_to_anchor=(1.15, 1))
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()

# ==================== 分析配置策略变化 ====================
print('配置策略变化:')
print(f'股票配置: 从{stocks_pct[0]:.0f}%降至{stocks_pct[-1]:.0f}%')
print(f'债券配置: 从{bonds_pct[0]:.0f}%升至{bonds_pct[-1]:.0f}%')
print('策略: 逐步从激进型转向稳健型')

57.9 金融应用杜邦分析

列表 57.8
# =============================================================================
# 题目:杜邦分析——ROE分解
# =============================================================================
# 本任务演示如何使用条形图展示杜邦分析的三个因子
# 应用场景:财务分析、ROE分解、公司对比

# ==================== 杜邦分析数据 ====================
# 杜邦分析公式:ROE = 销售净利率 × 总资产周转率 × 权益乘数
companies = ['公司A', '公司B', '公司C', '公司D', '公司E']

# ROE分解的三个因子
net_margin = [15, 10, 8, 20, 12]           # 销售净利率(%)
asset_turnover = [1.2, 1.5, 0.8, 1.0, 1.3]  # 总资产周转率
equity_multiplier = [2.0, 1.8, 2.5, 1.5, 2.2] # 权益乘数

# 计算ROE(净资产收益率)
# ROE = 销售净利率 × 总资产周转率 × 权益乘数 / 100(转换为百分比)
# 使用列表推导式计算每个公司的ROE
roe = [nm * at * em / 100 for nm, at, em in zip(net_margin, asset_turnover, equity_multiplier)]

# ==================== 创建双子图布局 ====================
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# ==================== 左图:ROE对比 ====================
# axes[0].bar()在第一个子图绘制ROE
axes[0].bar(companies, roe, color='#2E86AB', alpha=0.8)
axes[0].set_title('ROE对比', fontsize=14, fontweight='bold')
axes[0].set_ylabel('ROE (%)', fontsize=12)
# axes[0].set_ylim(0, max(roe) * 1.2):设置Y轴范围为0到最大值的1.2倍
axes[0].set_ylim(0, max(roe) * 1.2)
axes[0].grid(axis='y', alpha=0.3)

# 添加ROE数值标签
for i, v in enumerate(roe):
    axes[0].text(i, v + 1, f'{v:.1f}%', ha='center', va='bottom')

# ==================== 右图:杜邦三因子 ====================
# 准备X轴位置
x = np.arange(len(companies))
# 条形宽度
width = 0.25

# 绘制三个因子的分组条形图
# 第一组:销售净利率
axes[1].bar(x - width, net_margin, width, label='销售净利率(%)',
           color='#E3120B', alpha=0.8)

# 第二组:总资产周转率(×10,使量级与其他因子相当)
axes[1].bar(x, [at * 10 for at in asset_turnover], width,
           label='总资产周转率(x10)', color='#008080', alpha=0.8)

# 第三组:权益乘数(×5,使量级与其他因子相当)
axes[1].bar(x + width, [em * 5 for em in equity_multiplier], width,
           label='权益乘数(x5)', color='#F0A700', alpha=0.8)

axes[1].set_title('杜邦三因子', fontsize=14, fontweight='bold')
axes[1].set_xticks(x)
axes[1].set_xticklabels(companies)
axes[1].legend(fontsize=10)
axes[1].grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

# ==================== 输出杜邦分析表格 ====================
print('杜邦分析:')
# f'{string:^width}':格式化输出,居中对齐
print(f'{"公司":^8s} {"ROE":^8s} {"净利率":^10s} {"周转率":^10s} {"权益乘数":^10s}')
print('-' * 50)
for i, company in enumerate(companies):
    print(f'{company:^8s} {roe[i]:^8.1f} {net_margin[i]:^10.1f} {asset_turnover[i]:^10.2f} {equity_multiplier[i]:^10.2f}')

杜邦分析的金融理论:

\[ \text{ROE} = \frac{\text{净利润}}{\text{股东权益}} = \frac{\text{净利润}}{\text{销售收入}} \times \frac{\text{销售收入}}{\text{总资产}} \times \frac{\text{总资产}}{\text{股东权益}} \]

三因子含义:

  1. 销售净利率:盈利能力,每1元销售带来的净利润
  2. 总资产周转率:效率,每1元资产创造的销售收入
  3. 权益乘数:杠杆,资产与权益的比率

分析要点:

  • 高ROE可能来自高利润率(如白酒)
  • 或高周转率(如零售)
  • 或高杠杆(如银行、地产)
  • 需结合行业特点综合分析

57.10 本章小结

要点:

  • 条形图比较分类数值:纵向 plt.bar(x, height, width=...) 适合时间顺序与少量类别,横向 plt.barh(y, width) 适合长标签与多类别排名
  • 平台任务的数据链:月末收盘价二维数组 → pd.DataFrame(data=closeprice, index=date, columns=name)(行=月份、列=指数) → df/df.shift(1)-1 得月度涨跌幅 → dropna 去掉首月 → iloc[i] 取第i个月的四个指数值作为一组条形高度
  • 分组条形图靠位置偏移实现:第 \(j\) 组条形中心在 \(i+(j-\frac{m+1}{2})w\),组内靠拢、组间留白,再配合 set_xticks(x)set_xticklabels(...)
  • 堆叠条形图靠 bottom 累加:后一层从前一层的顶部画起,用于展示构成;百分比堆叠图先把各层除以总数再堆叠
  • 杜邦分解 \(\text{ROE}=\text{销售净利率}\times\text{总资产周转率}\times\text{权益乘数}\),三因子量纲不同,同图比较时需缩放或分面板展示

易错点:

  • np.array 中按行给出的收盘价必须与 index(月份)对应、按列与 columns(指数)对应,行列颠倒会把”各期涨跌幅对比”画错成别的口径
  • df/df.shift(1)-1np.log(df/df.shift(1)) 分别是简单收益率与对数收益率,涨跌幅的口径要与题面一致
  • 平台任务1中数组的跨行书写是题面原样,复制到平台时不要破坏续行结构,判定以平台为准
  • plt.xticks 的标签数量必须与刻度位置数量一致,否则报 ValueError
  • barh 画横向条形图后,网格方向应改为 axis='x',沿用 axis='y' 会出现无效网格
  • 数值标注 plt.text 的偏移量是绝对量,数据量级变化后需同步调整,否则标注会与柱顶重叠或飞出图外

57.11 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 自测回忆:不看正文,复述条形图与直方图在数据类型、X轴含义、条形间距、排列顺序四个方面的差异;并回答类别名称很长时应在 barbarh 之间选哪一个、为什么。

    参考答案(点开前请先独立完成)

    解题思路:四个方面的差异——数据类型:条形图比较分类变量的汇总值(均值、占比等),直方图展示数值变量的分布;X 轴含义:条形图的 X 轴是类别(无序、可按数值重排),直方图的 X 轴是连续的数值区间(有序、条形按区间自然排列);条形间距:条形图的条形之间有间隔(类别离散),直方图的条形相连(区间衔接);排列顺序:条形图常按数值降序排列便于读排名,直方图必须按区间升序排列。类别名称很长时选 barh(水平条形图):长标签放在 Y 轴左侧有充足的横向书写空间,条形长短仍沿水平方向可比;纵向 bar 的长标签要么倾斜挤占画幅、要么换行重叠。

    回扣主线:“条形图用于分类比较、直方图用于数值分布”的分工见本章”条形图 vs 直方图”一节与第 章节 20 章选型要点。

  2. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import pandas as pd
    df = pd.DataFrame({'A': [10.0, 11.0, 12.1], 'B': [20.0, 22.0, 24.2]})
    ret = df / df.shift(1) - 1
    print(ret.iloc[0].tolist())
    print(ret.dropna().shape)
    print(round(ret['A'].iloc[-1], 4))

    参考答案(先写下你的预测再点开)

    解题思路df.shift(1) 整体下移一行,首行变 NaN,NaN - 1df/NaN 仍是 NaN,故首行收益率为 [nan, nan]dropna() 删除首行后剩 2 行 2 列,即 (2, 2);A 列最后一个收益率是 12.1/11 − 1 = 0.1(12.1 恰为 11 的 1.1 倍),保留 4 位小数为 0.1。

    # 验证脚本:涨跌幅计算链的三个关键量
    import pandas as pd  # 导入pandas库
    df = pd.DataFrame({'A': [10.0, 11.0, 12.1], 'B': [20.0, 22.0, 24.2]})  # 两列月末价格
    ret = df / df.shift(1) - 1  # 简单收益率(涨跌幅)
    print(ret.iloc[0].tolist())  # 首行因shift(1)为NaN
    print(ret.dropna().shape)  # 删除首行后的形状
    print(round(ret['A'].iloc[-1], 4))  # A列最后一期收益率

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    [nan, nan]
    (2, 2)
    0.1

    回扣主线df/df.shift(1)-1 的数据链见本章”要点”第 2 条,与平台任务一致。

  3. 输出预测:分组条形图的位置由偏移决定,先预测输出,再上机检验。

    import numpy as np
    x = np.arange(3)
    width = 0.25
    print((x - width).round(2))
    print((x + width).round(2))

    参考答案(先写下你的预测再点开)

    解题思路x = [0, 1, 2],向左、向右各平移一个条宽 0.25——x - width 为 [−0.25, 0.75, 1.75],x + width 为 [0.25, 1.25, 2.25]。四个系列以 0.25 为步长在整数位置两侧对称排开,组内靠拢、组间留白。

    # 验证脚本:条形中心的左右偏移
    import numpy as np  # 导入NumPy库
    x = np.arange(3)  # 三个类别的基准位置
    width = 0.25  # 单根条形的宽度(兼作偏移步长)
    print((x - width).round(2))  # 左移一个条宽后的中心
    print((x + width).round(2))  # 右移一个条宽后的中心

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    [-0.25  0.75  1.75]
    [0.25 1.25 2.25]

    回扣主线:分组条形图的位置偏移公式见本章”分组条形图”一节”要点”第 3 条。

  4. 变式任务(平台任务同型改造):平台任务2把四个月份的涨跌幅分画在2×2子图中;请改为一张图上的分组条形图——X轴为四个指数,每个月份一组条形(用 x + k*width 偏移),图例区分月份,并对比两种形式各自的读图优势。

    参考答案(点开前请先独立完成)

    解题思路:按平台任务同款数据(4 个月 × 4 个指数的月末收盘价)先算涨跌幅,再把每个月份的一组条形以 x + (k - 1.5) * width 偏移画到同一坐标轴上。读图优势对比:2×2 分面把每个月份单独成图,月内四个指数的高矮对比最清晰,但跨月比较要在两图之间往返;分组条形图把同一个月的四根条形靠在一起,同一指数跨月的升降走势(如恒生指数 5 月 −9.42% 到 6 月 +6.10% 的大幅反弹)与同月内的横比一图完成,代价是类别多时组内条形偏密、单根条形变窄。

    # 变式脚本:四月份涨跌幅的分组条形图
    import matplotlib.pyplot as plt  # 导入绘图库
    import pandas as pd  # 导入pandas库
    import numpy as np  # 导入NumPy库
    date = ['2019年4月', '2019年5月', '2019年6月', '2019年7月', '2019年8月']  # 月份列表
    name = ['标普500指数', '富时100指数', '恒生指数', '上证综指']  # 指数列表
    closeprice = np.array([[2945.83,7418.22,29699.11,3078.34],[2752.06,7161.71,26901.09,2898.70],[2941.76,7425.63,28542.62,2978.88],[2980.38,7586.78,27777.75,2932.51],[2926.46,7207.18,25724.73,2886.24]])  # 月末收盘价
    index_data = pd.DataFrame(closeprice, index=date, columns=name)  # 月末收盘价数据框
    index_return = (index_data / index_data.shift(1) - 1).dropna()  # 月度涨跌幅
    print(index_return.round(4))  # 先输出涨跌幅表
    x = np.arange(4)  # 四个指数的基准位置
    width = 0.2  # 条宽兼偏移步长
    for k in range(4):  # 依次画四个月份
        plt.bar(x + (k - 1.5) * width, index_return.iloc[k], width, label=date[k + 1])  # 位置偏移+图例
    plt.xticks(x, name)  # 刻度位置与指数名对齐
    plt.axhline(0, color='k', lw=0.8)  # 画零基准线
    plt.legend()  # 显示图例
    plt.savefig('grouped_bar.png')  # 保存图形

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

             标普500指数  富时100指数    恒生指数    上证综指
    2019年5月  -0.0658  -0.0346 -0.0942 -0.0584
    2019年6月   0.0689   0.0369  0.0610  0.0277
    2019年7月   0.0131   0.0217 -0.0268 -0.0156
    2019年8月  -0.0181  -0.0500 -0.0739 -0.0158

    图形:四个指数位置各有一组四根条形(黄、青、蓝、红区分月份),正负条形分居零线两侧,恒生指数 5 月的长负条与 6 月的正条对比最醒目。

    注意:以上为变式代码;列表 57.2 的平台任务仍须按原始代码原样输入教学平台。

    回扣主线:分组条形图的偏移公式与 set_xticks 配合见本章”分组条形图”一节”要点”第 3 条。

  5. 思考题:堆叠条形图为什么不适合比较”各部分的大小”?请从每个条形段的起点(基线)出发解释,并给出两种更适合的替代图形。

    参考答案(点开前请先独立完成)

    解题思路:条形长度比较依赖”共同基线”——人眼对从同一起点量起的长度差敏感,对起点错位的段长几乎不敏感。堆叠条形图中只有最底层的段从零基线起画,其上每一层的起点都被前面的层抬高到了各自不同的高度,于是第 2、3 层的段虽然可以逐段读出长度,却缺少共同参照,跨条形比较各部分大小时视觉判断严重失真;当各条总量接近时问题更突出。两种更适合的替代图形:分组条形图——每个部分一根独立条形、全部从零基线起跳,长短直接可比;按数值降序排列的(水平)条形图——把”各部分的大小”变成同基线下的排名阅读,类别名长时用 barh 更佳。

    回扣主线:“堆叠图看构成、分组图比大小”的分工见本章”堆叠条形图”一节与第 章节 20 章。